/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {ts, ArkFile} from 'arkanalyzer/lib';
import {BaseChecker, BaseMetaData} from '../BaseChecker';
import {Defects, IssueReport} from '../../model/Defects';
import {AstMatcher, MatcherCallback, MatcherTypes} from '../../matcher/Matchers';
import {Utils} from '../../Index';
import {Rule} from '../../model/Rule';
import {RuleListUtil} from '../../utils/common/DefectsList';
import {RuleFix} from '../../model/Fix';
import { Language } from 'arkanalyzer/lib/core/model/ArkFile';

type Options = {
  allowExplicitAny: boolean
};

const gmetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: 'docs/no-implicit-any-catch.md', // TODO: support url
    description: 'Implicit any in catch clause.'
};

export class NoImplicitAnyCatchCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gmetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private issueMap: Map<string, IssueReport> = new Map();
    private defaultOptions: Options = {
        allowExplicitAny: false
    };
    private options: Options = { ...this.defaultOptions };
    private buildMatcher: AstMatcher = {
        matcherType: MatcherTypes.AST,
        match(node: ts.Node): boolean {
            return ts.isCatchClause(node);
        }
    };

    public registerMatchers(): MatcherCallback[] {
        const matchBuildCb: MatcherCallback = {
            matcher: this.buildMatcher,
            callback: this.check
        };
        return [matchBuildCb];
    };

    private parseOptions(): void {
        if (this.rule?.option?.[0]) {
            const ruleOption = this.rule.option[0] as Options;
            this.options = {
                allowExplicitAny: ruleOption.allowExplicitAny ?? this.defaultOptions.allowExplicitAny
            };
        };
    }

    private isValidFile(file: ArkFile): boolean {
        const lang = file.getLanguage();
        return lang === Language.TYPESCRIPT || lang === Language.JAVASCRIPT;
    }

    public check = (node: ts.CatchClause, file: ArkFile): void => {
        if (!this.isValidFile(file)) {
            return;
        }
        this.parseOptions();
        this.checkCatchParameter(node, file);
        this.reportSortedIssues();
    };

    private checkCatchParameter(node: ts.CatchClause, file: ArkFile): void {
        const variableDeclaration = node.variableDeclaration;
        const text = variableDeclaration?.getText();
        let fixText: string | undefined = this.determineFixText(variableDeclaration, text);
        if (!fixText) {
            return;
        }

        this.metaData.description = variableDeclaration?.type?.getText().toLowerCase() === 'any'
            ? 'Explicit any in catch clause.'
            : 'Implicit any in catch clause.'; 
        const sourceFile = node.getSourceFile();
        const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
        const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
        const defect = this.addIssueReport(file.getFilePath(), start.line + 1, start.character + 1, end.character + 1);

        const variableDeclStart = variableDeclaration?.getStart();
        const variableDeclEnd = variableDeclaration?.getEnd();
        if (variableDeclStart && variableDeclEnd && defect) {
            let fix: RuleFix = this.ruleFix(variableDeclStart, variableDeclEnd, fixText);
            this.issueMap.set(defect.fixKey, { defect, fix });
        }
    }
    
    private addIssueReport(filePath: string, startLine: number, startCol: number, endCol: number): Defects | void {
        const severity = this.rule.alert ?? this.metaData.severity;
        let defects = new Defects(startLine, startCol, endCol, this.metaData.description, 
            severity, this.rule.ruleId, filePath, this.metaData.ruleDocPath, true, false, true);
        this.defects.push(defects);
        return defects;
    }

    private determineFixText(variableDeclaration: ts.VariableDeclaration | undefined,
                             text: string | undefined): string | undefined {
        if (!variableDeclaration) {
            return undefined;
        }
        const typeText = variableDeclaration.type?.getText();
        if (!this.options.allowExplicitAny && typeText === 'any') {
            return `${text?.slice(0, text.indexOf(':'))}: unknown`; // 显式 any
        }
        return typeText ? undefined : `${text}: unknown`; // 隐式 any
    }

    private ruleFix(pos: number, end: number, fixText: string): RuleFix {
        return {range: [pos, end], text: fixText};
    }

    private reportSortedIssues(): void {
        if (this.issueMap.size === 0) {
            return;
        }
        const sortedIssues = Array.from(this.issueMap.entries())
            .sort(([keyA], [keyB]) => Utils.sortByLineAndColumn(keyA, keyB));
        this.issues = [];
        sortedIssues.forEach(([_, issue]) => {
            RuleListUtil.push(issue.defect);
            this.issues.push(issue);
        });
    }
}
